import { } from "@/tiptap";
import type { EditorView } from "@/tiptap/pm";
import {
  Decoration,
  DecorationSet,
  Fragment,
  Node,
  NodeSelection,
  NodeType,
  Plugin,
  PluginKey,
  ResolvedPos,
  Slice,
} from "@/tiptap/pm";
import { Editor, Extension } from "@/tiptap/vue-3";
import type { DraggableItemType, ExtensionOptions } from "@/types";
import { throttle } from "lodash-es";

// https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_and_Drop_API
// https://github.com/ueberdosis/tiptap/blob/7832b96afbfc58574785043259230801e179310f/demos/src/Experiments/GlobalDragHandle/Vue/DragHandle.js
export interface ActiveNode {
  $pos: ResolvedPos;
  node: Node;
  el: HTMLElement;
  offset: number;
  domOffsetLeft: number;
  domOffsetTop: number;
}

// 使用 WeakMap 为每个编辑器实例存储独立的状态
const editorStates = new WeakMap<EditorView, {
  draggableItem: DraggableItemType | boolean | undefined;
  draggableHandleDom: HTMLElement | null;
  activeNode: ActiveNode | null;
  activeSelection: NodeSelection | null;
  mouseleaveTimer: any;
  dragging: boolean;
  hoverOrClickDragItem: boolean;
}>();

// 获取或创建编辑器状态（优化版本，避免重复查找）
const getEditorState = (view: EditorView) => {
  let state = editorStates.get(view);
  if (!state) {
    state = {
      draggableItem: undefined,
      draggableHandleDom: null,
      activeNode: null,
      activeSelection: null,
      mouseleaveTimer: null,
      dragging: false,
      hoverOrClickDragItem: false,
    };
    editorStates.set(view, state);
  }
  return state;
};

const createDragHandleDom = () => {
  const dom = document.createElement("div");
  dom.classList.add("draggable");
  dom.draggable = true;
  dom.setAttribute("data-drag-handle", "true");
  return dom;
};

const showDragHandleDOM = (draggableHandleDom: HTMLElement | null) => {
  draggableHandleDom?.classList?.add("show");
  draggableHandleDom?.classList?.remove("hide");
};

const hideDragHandleDOM = (draggableHandleDom: HTMLElement | null) => {
  draggableHandleDom?.classList?.remove("show");
  draggableHandleDom?.classList?.remove("active");
  draggableHandleDom?.classList?.add("hide");
};

/**
 * Correct the position of draggableHandleDom to match the current position of activeNode.
 *
 * @param view
 * @param referenceRectDOM
 */
const renderDragHandleDOM = (
  view: EditorView,
  activeNode: ActiveNode | undefined,
  draggableHandleDom: HTMLElement | null
) => {
  const root = view.dom.parentElement;
  if (!root) {
    return;
  }

  if (!draggableHandleDom) {
    return;
  }

  const referenceRectDOM = activeNode?.el;
  if (!referenceRectDOM) {
    return;
  }

  const targetNodeRect = referenceRectDOM.getBoundingClientRect();
  const rootRect = root.getBoundingClientRect();
  const handleRect = draggableHandleDom.getBoundingClientRect();

  const left =
    targetNodeRect.left -
    rootRect.left -
    handleRect.width -
    1 +
    activeNode.domOffsetLeft;
  const top =
    targetNodeRect.top -
    rootRect.top +
    handleRect.height / 2 +
    root.scrollTop +
    activeNode.domOffsetTop;
  draggableHandleDom.style.left = `${left}px`;
  draggableHandleDom.style.top = `${top - 2}px`;

  showDragHandleDOM(draggableHandleDom);
};

const createHandleMouseEnterEvent = (view: EditorView) => () => {
  const state = getEditorState(view);
  if (!state.activeNode) {
    return;
  }
  state.hoverOrClickDragItem = true;
  view.dispatch(view.state.tr);
  clearTimeout(state.mouseleaveTimer);
  showDragHandleDOM(state.draggableHandleDom);
};

const createHandleMouseLeaveEvent = (view: EditorView) => () => {
  const state = getEditorState(view);
  if (!state.activeNode) {
    return;
  }
  state.hoverOrClickDragItem = false;
  view.dispatch(view.state.tr);
  hideDragHandleDOM(state.draggableHandleDom);
};

const createHandleMouseDownEvent = (view: EditorView) => () => {
  const state = getEditorState(view);
  if (!state.activeNode) {
    return null;
  }
  state.hoverOrClickDragItem = false;
  view.dispatch(view.state.tr);
  if (NodeSelection.isSelectable(state.activeNode.node)) {
    const nodeSelection = NodeSelection.create(
      view.state.doc,
      state.activeNode.$pos.pos - state.activeNode.offset
    );
    view.dispatch(
      view.state.tr.setSelection(nodeSelection)
    );
    view.focus();
    state.activeSelection = nodeSelection;
    return nodeSelection;
  }
  return null;
};

const createHandleMouseUpEvent = (view: EditorView) => () => {
  const state = getEditorState(view);
  if (!state.dragging) return;

  state.dragging = false;
  state.activeSelection = null;
  state.activeNode = null;
};

const createHandleDragStartEvent = (view: EditorView) => (event: DragEvent) => {
  const state = getEditorState(view);
  state.dragging = true;
  state.hoverOrClickDragItem = false;
  if (event.dataTransfer && state.activeNode && state.activeSelection) {
    const slice = state.activeSelection.content();
    event.dataTransfer.effectAllowed = "move";

    // @ts-ignore
    const { dom, text } = view.serializeForClipboard(slice);
    event.dataTransfer.clearData();
    event.dataTransfer.setData("text/html", dom.innerHTML);
    event.dataTransfer.setData("text/plain", text);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    event.dataTransfer.setDragImage(state.activeNode?.el as any, 0, 0);

    view.dragging = {
      slice,
      move: true,
    };
  }
};

const getDOMByPos = (
  view: EditorView,
  root: HTMLElement,
  $pos: ResolvedPos
) => {
  const { node } = view.domAtPos($pos.pos);

  let el: HTMLElement = node as HTMLElement;
  let parent = el.parentElement;
  while (parent && parent !== root && $pos.pos === view.posAtDOM(parent, 0)) {
    el = parent;
    parent = parent.parentElement;
  }

  return el;
};

const getPosByDOM = (
  view: EditorView,
  dom: HTMLElement
): ResolvedPos | null => {
  const domPos = view.posAtDOM(dom, 0);
  if (domPos < 0) {
    return null;
  }
  return view.state.doc.resolve(domPos);
};

export const selectAncestorNodeByDom = (
  dom: HTMLElement,
  view: EditorView
): ActiveNode | null => {
  const root = view.dom.parentElement;
  if (!root) {
    return null;
  }
  const $pos = getPosByDOM(view, dom);
  if (!$pos) {
    return null;
  }
  const node = $pos.node();
  const el = getDOMByPos(view, root, $pos);
  return { node, $pos, el, offset: 1, domOffsetLeft: 0, domOffsetTop: 0 };
};

const getExtensionDraggableItem = (editor: Editor, node: Node) => {
  const extension = editor.extensionManager.extensions.find((extension) => {
    return extension.name === node.type.name;
  });
  if (!extension) {
    return;
  }
  const draggableItem = (extension.options as ExtensionOptions).getDraggable?.({
    editor,
  });
  return draggableItem;
};

/**
 * According to the extension, obtain different rendering positions.
 *
 * @param editor
 * @param parentNode
 * @param dom
 * @returns
 **/
const getRenderContainer = (
  view: EditorView,
  draggableItem: DraggableItemType | undefined,
  dom: HTMLElement
): ActiveNode => {
  const renderContainer = draggableItem?.getRenderContainer?.({ dom, view });
  const node = selectAncestorNodeByDom(renderContainer?.el || dom, view);
  return {
    el: renderContainer?.el || dom,
    node: renderContainer?.node || (node?.node as Node),
    $pos: renderContainer?.$pos || (node?.$pos as ResolvedPos),
    offset: renderContainer?.nodeOffset || (node?.offset as number),
    domOffsetLeft: renderContainer?.dragDomOffset?.x || 0,
    domOffsetTop: renderContainer?.dragDomOffset?.y || 0,
  };
};

const findParentNodeByDepth = (
  view: EditorView,
  dom: HTMLElement,
  depth = 1
): Node | undefined => {
  const $pos = getPosByDOM(view, dom);
  if (!$pos) {
    return;
  }
  if (depth > $pos.depth) {
    // 解决 audio 等不是块元素的问题，目前仅寻找其直接第一个子节点
    if (depth - $pos.depth == 1) {
      const parentNode = $pos.node();
      if (parentNode.firstChild && !parentNode.firstChild.type.isBlock) {
        return parentNode.firstChild;
      }
    }
    return;
  }
  const node = $pos.node(depth);
  if (!node) {
    return;
  }
  return node;
};

const getDraggableItem = ({
  editor,
  view,
  dom,
  event,
  depth = 1,
}: {
  editor: Editor;
  view: EditorView;
  dom: HTMLElement;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  event?: any;
  depth?: number;
}): DraggableItemType | boolean | undefined => {
  const parentNode = findParentNodeByDepth(view, dom, depth);
  if (!parentNode) {
    return;
  }
  const draggableItem = getExtensionDraggableItem(editor, parentNode);
  if (draggableItem) {
    if (typeof draggableItem === "boolean") {
      return draggableItem;
    }
    const container = getRenderContainer(view, draggableItem, dom);
    const coords = { left: event.clientX, top: event.clientY };
    const pos = view.posAtCoords(coords);

    if (pos) {
      if (pos.inside == -1) {
        return draggableItem;
      }

      if (
        !(
          pos.inside >= container.$pos.start() &&
          pos.inside <= container.$pos.end()
        )
      ) {
        return draggableItem;
      }
    }

    if (draggableItem.allowPropagationDownward) {
      const draggable = getDraggableItem({
        editor,
        view,
        dom,
        event,
        depth: ++depth,
      });

      if (draggable) {
        return draggable;
      }
    }

    return draggableItem;
  }

  return getDraggableItem({
    editor,
    view,
    dom,
    event,
    depth: ++depth,
  });
};

/**
 * Get the insertion point of the target position relative to doc
 *
 * @param doc
 * @param pos
 * @param slice
 * @returns
 */
const dropPoint = (doc: Node, pos: number, slice: Slice) => {
  const $pos = doc.resolve(pos);
  if (!slice.content.size) {
    return pos;
  }
  let content: Fragment | undefined = slice.content;
  for (let i = 0; i < slice.openStart; i++) {
    content = content?.firstChild?.content;
  }
  for (
    let pass = 1;
    pass <= (slice.openStart == 0 && slice.size ? 2 : 1);
    pass++
  ) {
    for (let dep = $pos.depth; dep >= 0; dep--) {
      const bias =
        dep == $pos.depth
          ? 0
          : $pos.pos <= ($pos.start(dep + 1) + $pos.end(dep + 1)) / 2
            ? -1
            : 1;
      const insertPos = $pos.index(dep) + (bias > 0 ? 1 : 0);
      const parent = $pos.node(dep);
      let fits = false;
      if (pass == 1) {
        fits = parent.canReplace(insertPos, insertPos, content);
      } else {
        const firstChildType = content?.firstChild?.type;
        if (!firstChildType) {
          continue;
        }
        const wrapping = parent
          .contentMatchAt(insertPos)
          .findWrapping(firstChildType);
        fits =
          (wrapping && wrapping[0] &&
            parent.canReplaceWith(insertPos, insertPos, wrapping[0])) ||
          false;
      }
      if (fits) {
        return bias == 0
          ? $pos.pos
          : bias < 0
            ? $pos.before(dep + 1)
            : $pos.after(dep + 1);
      }
    }
  }
  return null;
};

const Draggable = Extension.create({
  name: "draggable",
  addProseMirrorPlugins() {
    return [
      new Plugin({
        key: new PluginKey("node-draggable"),
        view: (view) => {
          const state = getEditorState(view);
          state.draggableHandleDom = createDragHandleDom();

          const handleMouseEnter = createHandleMouseEnterEvent(view);
          const handleMouseLeave = createHandleMouseLeaveEvent(view);
          const handleMouseDown = createHandleMouseDownEvent(view);
          const handleMouseUp = createHandleMouseUpEvent(view);
          const handleDragStart = createHandleDragStartEvent(view);

          state.draggableHandleDom.addEventListener("mouseenter", handleMouseEnter);
          state.draggableHandleDom.addEventListener("mouseleave", handleMouseLeave);
          state.draggableHandleDom.addEventListener("mousedown", handleMouseDown);
          state.draggableHandleDom.addEventListener("mouseup", handleMouseUp);
          state.draggableHandleDom.addEventListener("dragstart", handleDragStart);

          const viewDomParentNode = view.dom.parentNode as HTMLElement;
          viewDomParentNode.appendChild(state.draggableHandleDom);
          viewDomParentNode.style.position = "relative";
          return {
            update: (view) => {
              // 更新已在 getEditorState 中处理
            },
            destroy: () => {
              const state = getEditorState(view);
              if (!state.draggableHandleDom) {
                return;
              }
              clearTimeout(state.mouseleaveTimer);
              state.draggableHandleDom.removeEventListener("mouseenter", handleMouseEnter);
              state.draggableHandleDom.removeEventListener("mouseleave", handleMouseLeave);
              state.draggableHandleDom.removeEventListener("mousedown", handleMouseDown);
              state.draggableHandleDom.removeEventListener("mouseup", handleMouseUp);
              state.draggableHandleDom.removeEventListener("dragstart", handleDragStart);
              state.draggableHandleDom.remove();
              editorStates.delete(view);
            },
          };
        },
        props: {
          handleDOMEvents: {
            // @ts-ignore
            mousemove: throttle((view: EditorView, event: MouseEvent) => {
              const state = getEditorState(view);
              const coords = { left: event.clientX, top: event.clientY };
              const pos = view.posAtCoords(coords);
              if (!pos || !pos.pos) return false;

              const nodePos = pos.inside > -1 ? pos.inside : pos.pos;

              const nodeDom =
                view.nodeDOM(nodePos) ||
                view.domAtPos(nodePos)?.node ||
                event.target;

              if (!nodeDom) {
                hideDragHandleDOM(state.draggableHandleDom);
                return false;
              }

              let dom: HTMLElement | null = nodeDom as HTMLElement;
              while (dom && dom.nodeType === 3) {
                dom = dom.parentElement;
              }
              if (!(dom instanceof HTMLElement)) {
                hideDragHandleDOM(state.draggableHandleDom);
                return false;
              }
              const editor = this.editor;
              state.draggableItem = getDraggableItem({
                // @ts-ignore
                editor,
                view,
                dom,
                event,
              });
              // skip the current extension if getDraggable() is not implemented or returns false.
              if (!state.draggableItem) {
                return false;
              }
              if (typeof state.draggableItem === "boolean") {
                state.activeNode = selectAncestorNodeByDom(dom, view);
              } else {
                state.activeNode = getRenderContainer(view, state.draggableItem, dom);
              }
              if (!state.activeNode) {
                return;
              }
              renderDragHandleDOM(view, state.activeNode, state.draggableHandleDom);
              return false;
            }, 100),
            mouseleave: throttle((view: EditorView) => {
              const state = getEditorState(view);
              clearTimeout(state.mouseleaveTimer);
              state.mouseleaveTimer = setTimeout(() => {
                hideDragHandleDOM(state.draggableHandleDom);
              }, 800);
              return false;
            }, 100),
          },
          handleKeyDown(view: EditorView) {
            const state = getEditorState(view);
            if (!state.draggableHandleDom) return false;
            state.draggableItem = undefined;
            hideDragHandleDOM(state.draggableHandleDom);
            return false;
          },
          handleDrop: (view, event, slice) => {
            const state = getEditorState(view);
            if (!state.draggableHandleDom) {
              return false;
            }
            if (!state.activeSelection) {
              return false;
            }
            const eventPos = view.posAtCoords({
              left: event.clientX,
              top: event.clientY,
            });
            if (!eventPos) {
              return true;
            }

            const $mouse = view.state.doc.resolve(eventPos.pos);
            const insertPos = dropPoint(view.state.doc, $mouse.pos, slice);
            if (!insertPos) {
              return false;
            }

            let isDisableDrop = false;
            if (state.dragging) {
              if (typeof state.draggableItem !== "boolean") {
                const handleDrop = state.draggableItem?.handleDrop?.({
                  view,
                  event,
                  slice,
                  insertPos,
                  node: state.activeNode?.node as Node,
                  selection: state.activeSelection,
                });
                if (typeof handleDrop === "boolean") {
                  isDisableDrop = handleDrop;
                }
              }
            }
            state.dragging = false;
            state.draggableItem = undefined;
            state.activeSelection = null;
            state.activeNode = null;
            return isDisableDrop;
          },
          decorations(editorState) {
            // @ts-ignore
            const view = this.view as EditorView;
            if (!view) {
              return DecorationSet.empty;
            }
            const state = getEditorState(view);
            if (!state.hoverOrClickDragItem || !state.activeNode) {
              return DecorationSet.empty;
            }
            const { $pos } = state.activeNode;
            return DecorationSet.create(editorState.doc, [
              Decoration.node($pos.before(), $pos.after(), {
                class: "has-draggable-handle",
              }),
            ]);
          },
        },
      }),
    ];
  },
});

export default Draggable;
